/* License Notice:
**
** This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
** This program is distributed in the hope that it will be useful,
**   but WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
**   GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
**   along with this program. If not, see <https://www.gnu.org/licenses/>.
**
** Originally this code was part of Breakout, now part of ArcadeFighter library.
** Breakout is free software: you can redistribute it and/or modify
**   it under the terms of the CC BY 4.0 license as published by
**   Creative Commons, either version 4 of the License, or (at your
**   option) any later version.
**
** Glad is published under the (WTFPL OR CC0-1.0) AND Apache-2.0 licenses.
*/

/**
 * @file sprite2D.hpp
 * @author TooOld2Rock'nRoll
 * @date 2022/Ago
 * @remark Requires a working copy for your system of the Glad library.
 *
 * @brief Sprite Class for 2D sprites for the ArcadeFigher library.
 *
 * @see https://learnopengl.com/In-Practice/2D-Game/Breakout
 * @see https://glad.dav1d.de/
 *
 * @todo May we change UV coordinates by just changing the original VBO????? (no need for extra UV vbo!)
 * @todo Change the Property getter names to getProperty[Name], the way it is now is confusing.
 * @todo Must find a way to render a border around the sprites, not just for debugging, but decoration some times.
 * @todo when image_atlas.xml is implemented on Texture, make a way to use the sprite/strip names to auto map frames to the sprite.
 * @todo WARNING! Draw function considers the same shader is being used all the time!!!! Is this  problem???
 * @todo to implement glTextureUnit, shader need to support -N textures at once and Sprite need a list of Textures....but what for?
 *       this seams a good trick fro a "custom" shader that prints the entire screen in one go since we don't have too many sprites and they all use sprite sheets.
 * @todo We are already taking advantage of "Image Atlas" (one large sprite sheet), need to study Texture Array!
 * @todo Is https://learnopengl.com/Advanced-OpenGL/Instancing useful for our use case? Probably not in 99% of the cases.
 */
#ifndef _SPRITE2D_HPP_
#define _SPRITE2D_HPP_

/*---- Includes ----*/
#include <memory>
#include <vector>
#include <atomic>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include "graphics/opengl.hpp"
#include "properties.hpp"
#include "texture.hpp"
#include "shader.hpp"

/*---- Class Declaration ----*/
/**
 * @brief Class to create and handle the use of 2D Sprites.
 *
 * This class main goal is to render a image on the screen, but to that end, it has to do A LOT!
 * Historically, a sprite represented the graphical objects in the screen that move "like ghosts" over the background,
 *    but the definition has changed over time to include any graphical object in a game.<br>
 * OpenGl has no formal definition of what sprites are, in practical terms it's just a surface with a image rendered
 *    on it. OpenGl also has no simplified interface for 2D graphics and offers the same tools (and its inherent
 *    complexities) that would be available for the far more demanding needs of 3D graphics handling.<br>
 *
 * To render a square on the screen, all you needs is a Sprite2D instance, set its size to something appropriate and
 *    a call to draw() with a valid Shader. But you most likely want to use images in your project and we recomend using
 *    our ResourceManager class to load a Texture and set it for the Sprite to use.<br>
 * After that, it's just a matter to combine the usual transformation methods to make something interesting with your ghosts.<br>
 *
 * Your project will, most likely, have to deal with sprite sheets too and the procedures used to map the Texture are not
 *    enough for OpenGl to know what to do with it. Since OpenGl "doesn't understand" pixels as coordinates, we still need
 *    a way to select those Texture areas and crop all the rest. For that, just use crop() passing the limits of the
 *    cropped area in the Texture or the specific tile you wish to use if the Texture was properly mapped first.<br>
 * Your sprite sheet is probably minimally optimized and contain much more than images for just one specific object in
 *    it. It may have ALL the props for the game or maybe all images for different states of a player and it may
 *    be in your best interest to also spread that complexity by also pre mapping the tiles that will be used by the sprite.<br>
 * In case all the tiles in the Texture should be used by the sprite, it is redundant to also map it in the Sprite. If
 *    the sprite sheet in question represents different objects of the game or different states of something, each sprite
 *    will use just a section of the Texture, use the other methods to specify which tiles will be needed.<br>
 *
 * To animate the sprite, just set a FPS different than 0 and it will cycle through the list of tiles, in order, on
 *    each update(). In this case, if the Sprite makes use of all the Texture's tiles, it will be necessary to also
 *    map the tiles for the Sprite.
 *
 * @warning Some OpenGl objects are not shareable among multiple contexts when using working threads to load resources
 *    in the background, like the sprite's Vector Array Objects. A workaround for this limitation is to load the textures
 *    in the working thread (which is the most time consuming process) and create the sprites in the main thread.
 *
 * @see https://learnopengl.com/Getting-started/Hello-Triangle
 * @see https://learnopengl.com/Getting-started/Transformations
 * @see https://www.khronos.org/opengl/wiki/OpenGL_Object#Object_Sharing
 */
class Sprite2D
{
    private:
        static TextureID_t last_texture;
        //static glTextureUnit last_glTexture;
        static VertexArrayObj_t last_vao;
        static glm::vec4 v4_last_tint; //to avoid setting the color uniform every loop

        //glTextureUnit glTexture;
        Texture *p_texture = nullptr;

        VertexArrayObj_t VAO = 0; //the identifier

        //texture mapping, which tiles from the map are we using and in which order to animate if required.
        std::vector<unsigned> *v_texture_map = nullptr;


    protected:
        VertexBuffObj_t txVBO = 0; ///<The Texture clipping area VBO identifier.

        ///The Texture clipping area coordinates.
        GLfloat f_texture_uv[8] = {0.f, 1.f,  //top left
                                   1.f, 0.f,  //bottom right
                                   0.f, 0.f,  //bottom left
                                   1.f, 1.f}; //top right

        Color v4_color_tint; ///<Color to apply over the texture or just fill the sprite area.

        //transformations
        Position v3_position; ///<The sprite position in world coordinates.
        Angle2D f_angle; ///<Angle to rotate the sprite.
        Size v3_size; ///<On screen size of the sprite

        glm::vec2 v2_flip = glm::vec2 (1.f); ///<Which axis should be flipped.

        bool b_model_dirty_flag = true; ///<Mark that the model matrix to be rebuilt.
        glm::mat4 m4_model_cash = glm::mat4 (1.f); ///<The Model Matrix to be applied over the sprite on the shader.

        //texture mapping
        static VertexBuffObj_t last_txVBO; ///<The last Texture clipping area VBO to be used, avoid binding again.

        //animation
        bool b_animation_loop = false; ///<If the animation should loop infinitely or cycle once and stop.
        float f_animation_refresh_t = 0.f; ///<Time between animation frame changes in seconds.
        float f_animation_delta_t = 0.f; ///<How much time has passed since last animation frame change.
        unsigned u_current_frame = 0; ///<The current frame being displayed, index of the tile in the Sprite's map.

        /** @brief Helper method to recalculate the Model matrix when any spacial attribute of the sprite changes. */
        void _rebuildModel ();


    public:
        Sprite2D ();
        virtual ~Sprite2D ();

        //delete copy and move constructor/assign operators to make optimizations safe
        Sprite2D (Sprite2D const&) = delete;            //Copy construct
        Sprite2D (Sprite2D&&) = delete;                 //Move construct
        Sprite2D& operator= (Sprite2D const&) = delete; //Copy assign
        Sprite2D& operator= (Sprite2D &&) = delete;     //Move assign

        //void setGLTexture (glTextureUnit glT) { this->glTexture = glT; }
        void setTexture (Texture *t);
        /** @brief Gets a pointer to the sprite's Texture. */
        Texture* getTexture () const { return this->p_texture; }

        /** @brief Gets the sprite color tint. */
        Color & getColorTint () { return this->v4_color_tint; }

        /** @brief Gets the sprite position property. */
        Position & getPosition () { return this->v3_position; }
        /** @brief Gets the sprite position property. */
        Angle2D & getAngle () { return this->f_angle; }
        /** @brief Gets the sprite size property. */
        Size & getSize () { return this->v3_size; }

        void mirror (bool arround_x, bool arround_y);

        virtual void draw (Shader &shader);

        /*** To use with Sprite Sheets ***/
        void crop (float x1, float y1, float x2, float y2);
        void crop (unsigned x1, unsigned y1, unsigned x2, unsigned y2, bool resize); //<-- it calculates the gl coordinates on the fly
        void cropUsingTxTileMap (unsigned tile_index, bool resize);

        void addAllTxTilesToList ();
        void addContiguousTxTilesToList (unsigned tx_start, unsigned tx_end);
        void addTxTileToList (unsigned tile_index);
        /** @brief Gets the index list of tiles the sprite is using from the Texture. */
        const std::vector<unsigned>* getTilesList () const { return this->v_texture_map; }

        void cropUsingSpTileMap (unsigned s_tile_index, bool resize);

        /*** To use with Animated Sprites ***/
        void setAnimationFPS (float fps, bool loop=false);
        void resetAnimation (bool reset_delta_t);
        virtual bool update (double delta_t);
        /** @brief Returns the current frame being rendered in the animation loop. */
        unsigned getFrameIndex () const { return this->u_current_frame; }
};//END Sprite2D

#endif //_SPRITE2D_HPP_

